package club.kingyin.kyblogprosearch.service.impl;

import club.kingyin.commons.clients.FileClient;
import club.kingyin.commons.clients.UserClient;
import club.kingyin.commons.entity.core.Document;
import club.kingyin.commons.entity.user.Message;
import club.kingyin.commons.enums.BlogEnum;
import club.kingyin.commons.mail.MailMessage;
import club.kingyin.commons.utils.Commons;
import club.kingyin.kyblogprosearch.client.ElasticClient;
import club.kingyin.kyblogprosearch.core.NetWorldCloud;
import club.kingyin.kyblogprosearch.service.DocumentService;
import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.*;
import co.elastic.clients.elasticsearch._types.analysis.*;
import co.elastic.clients.elasticsearch._types.query_dsl.Query;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.TotalHits;
import co.elastic.clients.elasticsearch.indices.AnalyzeRequest;
import co.elastic.clients.elasticsearch.indices.AnalyzeResponse;
import co.elastic.clients.elasticsearch.indices.GetIndexRequest;
import co.elastic.clients.elasticsearch.indices.analyze.AnalyzeToken;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import com.fasterxml.jackson.core.filter.TokenFilterContext;
import com.kennycason.kumo.CollisionMode;
import com.kennycason.kumo.WordFrequency;
import com.kennycason.kumo.bg.PixelBoundaryBackground;
import com.kennycason.kumo.font.KumoFont;
import com.kennycason.kumo.font.scale.SqrtFontScalar;
import com.kennycason.kumo.nlp.FrequencyAnalyzer;
import com.kennycason.kumo.palette.ColorPalette;
import javassist.compiler.ast.Keyword;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.print.Doc;
import java.awt.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class DocumentServiceImpl implements DocumentService {

    private static final String DOCUMENT = "document";
    private static final int REDO_COUNT = 3;

    @Value("${ky-blog-pro.avatar}")
    private String avatar;

    @Value("${ky-blog-pro.email}")
    private String email;

    @Resource
    private UserClient userClient;

    @Resource
    private FileClient fileClient;

    @Resource(name="HotRedisTemplate")
    private RedisTemplate<String,Object> redisTemplate;

    public boolean exists(String index) throws IOException {
        return ElasticClient.client().indices().exists(e -> e.index(index)).value();
    }

    @SneakyThrows
    public boolean hasDocument() {
        return exists(DOCUMENT);
    }

//    public String index() {
//        ElasticClient.client().indices()
//                .create(
//                        c->c.index(DOCUMENT)
//                                .mappings(
//                                        m->m.properties("all",
//                                                o->o.text(
//                                                text->text.analyzer("ik_max_word"))
//                                        ).properties("background",
//                                                o->o.keyword(
//                                                        keyword->keyword.index(false))
//                                        ).properties("classify",
//                                                o->o.keyword(k->k)
//                                        ).properties("context",
//                                                o->o.text(
//                                                        t->t.copyTo("all")
//                                                                .analyzer("html_text_analyzer")
//                                                                .searchAnalyzer("ik_max_word")
//                                                ))
//                                        .properties("id",
//                                                o->o.integer(i->i.index(false))
//                                        ).properties("slogan",
//                                                        o->o.text(t->t.copyTo("all")
//                                                        .analyzer("my_analyzer")
//                                                        .searchAnalyzer("ik_max_word"))
//                                                )
//                                        .properties("status",
//                                                o->o.keyword(k->k))
//                                        .properties("tag",
//                                                o->o.text(t->t.copyTo("all")
//                                                .analyzer("my_analyzer")
//                                                        .searchAnalyzer("ik_max_word")
//                                                )
//                                        ).properties("timestamp",
//                                                        o->o.long_(l->l))
//                                        .properties("title",
//                                                o->o.text(t->t.copyTo("all")
//                                                .analyzer("full_prefix_analyzer"))
//                                        ).properties("updatetime",
//                                                        o->o.long_(l->l))
//                                        .properties("userid",o->o.integer(i->i))
//                                        .properties("views",o->o.integer(i->i))
//                                        )
//                        .settings(
//                                s->s.index(
//                                        i->i.routing(
//                                                r->r.allocation(
//                                                        a->a.include(
//                                                                in->in.tierPreference("data_content")
//                                                        )
//                                                )
//                                        )
//                                ).numberOfShards("1")
//                                .providedName("document")
//                                .creationDate("1647005500266")
//                                .analysis(
//                                        a->a.filter("pinyin_first_letter_and_full_pinyin_filter", o->o.definition("",)
//                                        )
//                        )
//                )
//    }


    // 文章推荐
    @SneakyThrows
    public List<Document> recommend(Document document) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .query(q->q
                        .bool(b->b
                                .mustNot(status->status
                                        .term(t->t.field("status").value(v->v.stringValue("DRAFT")))
                                        )
                                .mustNot(ids->ids
                                        .ids(id->id.values(String.valueOf(document.getId())))
                                )
                                .must(mm->mm

                                        .multiMatch(query->query
                                                .query(document.getTitle()+document.getSlogan())
                                        )
                                )
                                .must(m->m
                                        .match(tag->tag
                                                .field("tag")
                                                .query(v->v.stringValue(document.getTag()))
                                        )
                                )
                        )
                )
                .source(s->s.filter(f->f.includes("title","id","background","views","updatetime","slogan")))
                .from(0)
                .size(2)
                .sort(s->s
                        .script(sc->sc
                                .script(math->math
                                        .inline(i->i.source("Math.random()"))
                                )
                                .order(SortOrder.Asc)
                                .type(ScriptSortType.Number)
                        )
                )
                .build();
        return getDocuments(searchRequest);
    }

    @SneakyThrows
    public List<String> analyzer(String context) {
        AnalyzeRequest request = new AnalyzeRequest.Builder().analyzer("ik_smart").text(context).build();
        AnalyzeResponse analyze = ElasticClient.client().indices().analyze(request);
        List<String> res = new ArrayList<>();
        for (AnalyzeToken token : analyze.tokens()) {
            res.add(token.token());
        }
        return res;
    }

    @SneakyThrows
    public List<Document> searchTag(String tag, int from, int size) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(from).size(size)
                .query(q->q
                        .bool(mt->mt
                                .mustNot(term->term
                                        .term(s->s.
                                                field("status")
                                                .value(v->v.stringValue("DRAFT"))
                                        ))
                                .must(mm->mm
                                        .match(query->query
                                                .field("tag")
                                                .query(t->t.stringValue(tag))
                                        )
                                )
                        )
                )
                .sort(s->s
                        .field(time->time.field("updatetime").order(SortOrder.Desc))
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id","slogan","background","tag","views","updatetime")
                        ))
                .build();
        return getDocuments(searchRequest);
    }

    @SneakyThrows
    @Override
    public List<Document> searchClassify(String classify, int from, int size) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(from).size(size)
                .query(q->q
                        .bool(mt->mt
                                .mustNot(term->term
                                        .term(s->s.
                                                field("status")
                                                .value(v->v.stringValue("DRAFT"))
                                        ))
                                .must(mm->mm
                                        .term(query->query
                                                .field("classify")
                                                .value(c->c.stringValue(classify))
                                        )
                                )
                        )
                )
                .sort(s->s
                        .field(time->time.field("updatetime").order(SortOrder.Desc))
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id","slogan","background","tag","views","updatetime")
                        ))
                .build();
        return getDocuments(searchRequest);
    }

    @Override
    @SneakyThrows
    public List<Document> searchByIds(Set<String> ids) {
        List<String> idStrings = new ArrayList<>();
        idStrings.addAll(ids);
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .query(q->q
                        .ids(id->id.values(idStrings)
                        )
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id","slogan","tag","classify","updatetime","views","background")
                        ))
                .build();
        return getDocuments(searchRequest);
    }

    // 全文检索
    @SneakyThrows
    public List<Document> searchAll(String context, int from, int size) {

        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(from).size(size)
                .query(q->q
                        .bool(mt->mt
                                .mustNot(term->term
                                        .term(s->s.
                                                field("status")
                                                .value(v->v.stringValue("DRAFT"))
                                        ))
                                .must(mm->mm
                                        .multiMatch(query->query
                                                .query(context)
                                                .fields("title","slogan","tag","context")
                                        )
                                )
                        )
                )
                .highlight(h->h
                        .fields("tag",tag->tag.requireFieldMatch(false))
                        .fields("slogan",slogan->slogan.requireFieldMatch(false))
                        .preTags("<em style='color:red;font-style:normal;'>")
                        .postTags("</em>")
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id","slogan","background","tag","views","updatetime")
                        ))
                .build();
        return getDocuments(searchRequest);
    }

    // 搜索所有标题
    @SneakyThrows
    public List<Document> searchAllTitle(String title) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(0).size(15)
                .query(q->q
                        .match(m->m
                                .field("title")
                                .query(v->v.stringValue(title))
                        )
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id")
                        ))
                .build();
        return getDocuments(searchRequest);
    }

    private List<Document> getDocuments(SearchRequest searchRequest) throws IOException {

        SearchResponse<Document> personSearchResponse = ElasticClient.client().search(searchRequest, Document.class);
        TotalHits total = personSearchResponse.hits().total();
        List<Hit<Document>> hits = personSearchResponse.hits().hits();
        List<Document> documents = new ArrayList<>();
        for (Hit<Document> hit : hits) {
            Document source = hit.source();
            source.setContext(String.valueOf(total.value()));
            List<String> slogan = hit.highlight().get("slogan");
            if (slogan != null) {
                assert source != null;
                source.setSlogan(slogan.get(0));
            }
            List<String> tag = hit.highlight().get("tag");
            if (tag != null) {
                assert source != null;
                source.setTag(tag.get(0));
            }
            documents.add(source);
        }
        return documents;
    }


    // 搜索用户标题
    @SneakyThrows
    public List<Document> searchUserTitle(String title, int userid) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(0).size(15)
                .query(q->q
                        .bool(b->b
                                .must(m->m
                                        .term(t->t
                                                .field("userid")
                                                .value(v->v.longValue(userid))
                                        ))
                                .must(m->m
                                        .match(t->t
                                                .query(name->name.stringValue(title))
                                                .field("title")
                                        )
                                )
                        )
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id")
                        ))
                .build();
        return getDocuments(searchRequest);
    }

    @SneakyThrows
    @Override
    public Result save(Document document) {
        for (int i = 1; i <= REDO_COUNT; i++) {
            try {
                if(document.getTimestamp() == null) {
                    document.setTimestamp(0L);
                }
                IndexRequest<Document> build = new IndexRequest.Builder<Document>().index(DOCUMENT).id(String.valueOf(document.getId()))
                        .document(document).build();
                return ElasticClient.client().index(build).result();
            } catch (Exception e) {
                e.printStackTrace();
                log.error("保存失败，正在重试{}次",i);
                TimeUnit.SECONDS.sleep(1);
            }
        }
        sentErrMessage(document);
        sendErrMail(String.valueOf(document.getId()), document.getTitle());
        return null;
    }

    @SneakyThrows
    public Result update(Document document) {
        for (int i = 1; i <= REDO_COUNT; i++) {
            try {
                if(document.getTimestamp() == null) {
                    document.setTimestamp(0L);
                }
                UpdateRequest<Document, Document> personPersonUpdateRequest = new UpdateRequest.Builder<Document, Document>().index(DOCUMENT).id(String.valueOf(document.getId()))
                        .doc(document).build();
                UpdateResponse<Document> update = ElasticClient.client().update(personPersonUpdateRequest, Document.class);
                return update.result();
            } catch (Exception e) {
                e.printStackTrace();
                log.error("保存失败，正在重试{}次",i);
                TimeUnit.SECONDS.sleep(1);
            }
        }
        sentErrMessage(document);
        sendErrMail(String.valueOf(document.getId()), document.getTitle()==null?"标题位置": document.getTitle());
        return null;
    }

    private void sentErrMessage(Document document) {
        Message message = new Message();
        message.setAvatar(avatar);
        message.setObjectid(document.getId());
        message.setStatus(BlogEnum.UNREAD);
        message.setUserid(document.getUserid());
        message.setTimestamp(System.currentTimeMillis());
        message.setData("您的文章【"+document.getTitle()+"】保存失败了，请前往重新保存");
        userClient.sentMessage(message);
    }

    private void sendErrMail(String id, String title) {
        MailMessage mailMessage = new MailMessage();
        mailMessage.setUsernames(email);
        mailMessage.setSubject("ES 同步失败");
        String context = "⚠警告：ES同步数据失败</br>【<p style=\"color: #4A90E2; display: inline;\">"+title+"</p>】（文章ID：<p style=\"color: #4A90E2; display: inline;\">"+id+"</p>）</br>请尽快处理!";
        mailMessage.setContext(context);
        userClient.sendMailAsync(mailMessage);
    }

    @SneakyThrows
    @Override
    public Result delete(String id) {
        for (int i = 1; i <= REDO_COUNT; i++) {
            try {
                DeleteRequest build = new DeleteRequest.Builder().index(DOCUMENT).id(id).build();
                return ElasticClient.client().delete(build).result();
            } catch (Exception e) {
                log.error("保存失败，正在重试{}次",i);
                TimeUnit.SECONDS.sleep(1);
            }
        }
        sendErrMail(id,"文章数据已删除");
        return null;
    }

    /**
     * 随机取出N条数据
     * @param n N条
     * @param filterIds 需要过滤的ID
     * @return 文章集合
     */
    public List<Document> random(int n, List<String> filterIds) throws IOException {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .query(q->q
                        .bool(b->b
                                .mustNot(status->status
                                        .term(t->t.field("status").value(v->v.stringValue("DRAFT")))
                                )
                                .mustNot(ids->ids
                                        .ids(id->id.values(filterIds))
                                )
                        )
                )
                .source(s->s.filter(f->f.includes("title","id","background","views","updatetime","slogan","tag")))
                .from(0)
                .size(n)
                .sort(s->s
                        .script(sc->sc
                                .script(math->math
                                        .inline(i->i.source("Math.random()"))
                                )
                                .order(SortOrder.Asc)
                                .type(ScriptSortType.Number)
                        )
                )
                .build();
        List<Document> documents = getDocuments(searchRequest);
        for (Document document : documents) {
            filterIds.add(String.valueOf(document.getId()));
        }
        return documents;
    }

    @SneakyThrows
    public List<Document> hotDocument(List<String> filter) {
        Set<String> ids = new HashSet<>();
        List<Integer> integers = (List<Integer>) redisTemplate.opsForValue().get("hots-document");
        List<Document> documents = new ArrayList<>();
        if (integers != null) {
            for (Integer id : integers) {
                ids.add(id+"");
                filter.add(id+"");
            }
            documents = searchByIds(ids);
        }

        if (documents.size()<4) {
            documents.addAll(random(4- documents.size(),filter));
        }
        return documents;
    }

    private final List<String> cla = Arrays.asList("学习中心", "随心记录", "科学技术", "体育休闲", "游戏", "新青年", "设计创作", "时尚艺术", "生活娱乐", "其他");

    @SneakyThrows
    public List<Document> recommend(int userid, List<String> filter) {
        if (userid == 0) return random(8,filter);
        List<String> list = (List<String>) redisTemplate.opsForValue().get("word-count-" + userid);
        List<Document> documents = new ArrayList<>();
        List<FieldValue> curCla = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        if (list != null) {
            for (String s : list) {
                if (cla.contains(s)) {
                    curCla.add(new FieldValue.Builder().stringValue(s).build());
                }
                builder.append(builder);
            }
            List<Query> queryList = new ArrayList<>();
            queryList.add(new Query.Builder().match(tag -> tag
                    .field("tag")
                    .query(v -> v.stringValue(builder.toString())
                    )
            ).build());
            queryList.add(new Query.Builder().terms(cla -> cla.field("classify").terms(c -> c.value(curCla))).build());
            SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                    .query(q->q
                            .bool(b->b
                                    .mustNot(status->status
                                            .term(t->t.field("status").value(v->v.stringValue("DRAFT")))
                                    )
                                    .mustNot(ids->ids
                                            .ids(id->id.values(filter))
                                    )
                                    .must(mm->mm
                                            .multiMatch(query->query
                                                    .query(builder.toString())
                                            )
                                    )
                                    .must(queryList)
                            )
                    )
                    .source(s->s.filter(f->f.includes("title","id","background","views","updatetime","slogan","tag")))
                    .from(0)
                    .size(8)
                    .sort(s->s
                            .script(sc->sc
                                    .script(math->math
                                            .inline(i->i.source("Math.random()"))
                                    )
                                    .order(SortOrder.Asc)
                                    .type(ScriptSortType.Number)
                            )
                    )
                    .build();
            documents = getDocuments(searchRequest);
            for (Document document : documents) {
                filter.add(String.valueOf(document.getId()));
            }
        }
        if (documents.size() < 8) {
            documents.addAll(random(8- documents.size(),filter));
        }
        return documents;
    }

    @SneakyThrows
    public List<Document> lastTimestamp(List<String> filter) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(0).size(4)
                .query(q->q
                        .bool(mt->mt
                                .mustNot(term->term
                                        .term(s->s.
                                                field("status")
                                                .value(v->v.stringValue("DRAFT"))
                                        )
                                )
                                .mustNot(ids->ids
                                        .ids(id->id.values(filter))
                                )
                        )
                )
                .sort(s->s
                        .field(time->time.field("updatetime").order(SortOrder.Desc))
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id","slogan","background","tag","views","updatetime")
                        ))
                .build();
        List<Document> documents = getDocuments(searchRequest);
        for (Document document : documents) {
            filter.add(String.valueOf(document.getId()));
        }
        return documents;
    }

    @SneakyThrows
    public List<Document> moreViews(List<String> filter) {
        SearchRequest searchRequest = new SearchRequest.Builder().index(DOCUMENT)
                .from(0).size(4)
                .query(q->q
                        .bool(mt->mt
                                .mustNot(term->term
                                        .term(s->s.
                                                field("status")
                                                .value(v->v.stringValue("DRAFT"))
                                        )
                                )
                                .mustNot(ids->ids
                                        .ids(id->id.values(filter))
                                )
                        )
                )
                .sort(s->s
                        .field(time->time.field("views").order(SortOrder.Desc))
                )
                .source(s->s
                        .filter(f->f
                                .includes("title","id","slogan","background","tag","views","updatetime")
                        ))
                .build();
        List<Document> documents = getDocuments(searchRequest);
        for (Document document : documents) {
            filter.add(String.valueOf(document.getId()));
        }
        return documents;
    }

    final FrequencyAnalyzer frequencyAnalyzer = new FrequencyAnalyzer();
    public club.kingyin.commons.response.Result worldCloud(int userid) throws IOException {
        club.kingyin.commons.response.Result res = userClient.getHistoryByUserid(userid);
        if (!res.isSuccess()) return res;
        LinkedHashMap data = (LinkedHashMap) res.getData();
        List<Document> list = searchByIds(data.keySet());
        StringBuilder builder = new StringBuilder();
        for (Document document : list) {
            builder.append(document.getTitle());
//            builder.append(document.getSlogan());
            builder.append(document.getTag());
            builder.append(document.getClassify());
        }
        frequencyAnalyzer.setWordFrequenciesToReturn(300);
        frequencyAnalyzer.setMinWordLength(2);
        frequencyAnalyzer.setWordTokenizer(this::analyzer);
        ByteArrayInputStream inputStream = new ByteArrayInputStream(builder.toString().getBytes(StandardCharsets.UTF_8));
        final List<WordFrequency> wordFrequencies = frequencyAnalyzer.load(inputStream);
        // 保存前十个用户标签
        Set<String> tags = new HashSet<>();
        for (WordFrequency wordFrequency : wordFrequencies) {
            tags.add(wordFrequency.getWord());
            if (tags.size()>10) break;
        }
        redisTemplate.opsForValue().set("word-count-"+userid,tags);
        NetWorldCloud worldCloud = createWorldCloud(wordFrequencies);
        String filename = createWorldCloudName();
        MultipartFile file = Commons.getMultipartFile(worldCloud.writeToStream(), filename);
        return club.kingyin.commons.response.Result.success(fileClient.uploadBySystem(file,createUserWorldCloudPath(userid)+filename,"cloud"));
    }

    private NetWorldCloud createWorldCloud(List<WordFrequency> wordFrequencies) throws IOException {
        final Dimension dimension = new Dimension(550, 330);
        final NetWorldCloud wordCloud = new NetWorldCloud(dimension, CollisionMode.PIXEL_PERFECT);
        wordCloud.setPadding(2);
        PixelBoundaryBackground background = new PixelBoundaryBackground(
                Objects.requireNonNull(Commons.getInputStreamByUrl("http://dev.kingyin.club:9000/cloud/pic/whale_small.png")));
        wordCloud.setBackground(background);
        wordCloud.setKumoFont(new KumoFont(new Font("宋体", Font.PLAIN, 10)));
        wordCloud.setColorPalette(new ColorPalette(new Color(0x1f4888), new Color(0x3a4887), new Color(0x504887), new Color(0x6d4886)));
        wordCloud.setFontScalar(new SqrtFontScalar(12, 45));
        wordCloud.setBackgroundColor(Color.white);
        wordCloud.build(wordFrequencies);
        return wordCloud;
    }

    private String createUserWorldCloudPath(int userid) {
        return "/" + userid + "/系统/";
    }

    private String createWorldCloudName() {
        return "world-cloud.png";
    }
}
